#!/usr/bin/env python
# -*- coding: utf-8 -*-

# This file is part of Beremiz, a Integrated Development Environment for
# programming IEC 61131-3 automates supporting plcopen standard and CanFestival.
#
# Copyright (C) 2007: Edouard TISSERANT and Laurent BESSARD
# Copyright (C) 2017: Paul Beltyukov
#
# See COPYING file for copyrights details.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
import hashlib
import json
import operator
import os
import re
import shutil
import sys
import traceback
from concurrent import futures
from functools import reduce
from os.path import exists

import wx
from click import Context, Command
from jinja2 import Environment, FileSystemLoader
from loguru import logger
from pypinyin import lazy_pinyin
from syncasync import sync_to_async

from Beremiz.runtime.typemapping import DebugTypesSize
from Beremiz.targets.packageManger import package_get
from board.board_base import fw_plc, fw_rte
from board.board_root import boards
from globalobj import globalOBJ
from kv_platformio.kVpio import debug, run
from tool.tool_git import git

_ = wx.GetTranslation
includes_re = re.compile(r'\s*#include\s*["<]([^">]*)[">].*')
sys.path.append(os.path.dirname(sys.executable))
extra_flag = {'USE_NTPClient': ['LWIP_DNS']}
staticPackage = ['STM32Ethernet', 'modbus', 'NTPClient', "LwIP", "SPIFlash", "ShiftRegister74HC595", "Rtc_Pcf8563",
                 "Q2-HX711-Arduino-Library", "pt100rtd", "pt100_hx711", "MCPDAC", "littlefs", "FatFs", "debouncer",
                 "ch423", "STM32FreeRTOS", "USBStick_stm32", "STM32_USB_Host_Library", "ad7689", 'ArduinoJson',
                 'msgpack-arduino', 'ArduinoHttpClient']


def build_number(buildnumber):
    buildnumber_c = "src/buildnumber.c"
    f = open(buildnumber_c, "w+")
    f.write("const unsigned short build_number=%d;" % buildnumber)
    f.close()


def findObject(paths, test):
    path = None
    for p in paths:
        if test(p):
            path = p
            break
    return path


def _GetProgFlagFileName():
    app_path = os.getcwd()
    mt = os.path.abspath(os.path.join(app_path, 'prog.flag', ))
    return mt


class toolchain_platformio(object):
    """
    This abstract class contains GCC specific code.
    It cannot be used as this and should be inherited in a target specific
    class such as target_linux or target_win32
    """
    exe_path: str
    board: str
    model: str
    app: fw_plc
    rte: fw_rte
    srcmd5: {}

    def __init__(self, CTRInstance):
        self.CTRInstance = CTRInstance
        self.prjpath = self.CTRInstance.GetProjectPath()
        app_path = self.CTRInstance.GetIECLibPath()
        self.app_path = os.path.abspath(os.path.join(app_path, '../../..', ))
        self.buildpath = None
        self.upload_protocol = None
        self.options = []
        self.deps = []
        self.mode = 'APP'
        self.libs = []
        self.deps = []
        self.lib_other = []
        self.build_flags = ['-std=gnu11']
        self.static = False
        self.dlopen_prefix = ""
        self.extension = '.bin'
        self.platform_static = 'ststm32@10.0.73'
        self.platform_dev = 'https://e.coding.net/kaikong/rte_stm32h750/platform-ststm32.git#kaikong'
        self.exe = 'firmware.bin'
        self.framework = ''
        self.SetBuildPath(self.CTRInstance._getBuildPath())

    def getBuilderCFLAGS(self):
        """
        Returns list of builder specific CFLAGS
        """
        return [self.CTRInstance.GetTarget().getcontent().getCFLAGS()]

    def getBuilderLDFLAGS(self):
        """
        Returns list of builder specific LDFLAGS
        """
        return self.CTRInstance.LDFLAGS + [self.CTRInstance.GetTarget().getcontent().getLDFLAGS()]

    def getCompiler(self):
        """
        Returns compiler
        """
        return self.CTRInstance.GetTarget().getcontent().getCompiler()

    def getLinker(self):
        """
        Returns linker
        """
        return self.CTRInstance.GetTarget().getcontent().getLinker()

    def GetBinaryPath(self):
        self.SetBuildPath(self.CTRInstance._getBuildPath())
        return self.exe_path

    def GetBinaryCode(self):
        try:
            return open(self.exe_path, "rb").read()
        except Exception:
            return None

    def _GetMD5FileName(self):
        return os.path.join(self.prjpath, "lastbuildPLC.md5")

    def ResetBinaryMD5(self):
        try:
            os.remove(self._GetMD5FileName())
        except Exception:
            pass

    def GetBinaryMD5(self):
        try:
            return open(self._GetMD5FileName(), "r").read()
        except Exception:
            return None

    def SetBuildPath(self, buildpath):
        self.buildpath = buildpath
        # self.exe = self.CTRInstance.GetProjectName() + self.extension
        self.exe_path = os.path.join(self.prjpath, '.pio', 'build', 'plcapp', self.exe)
        self.srcmd5 = {}

    def append_cfile_deps(self, src, deps):
        for lN in src.splitlines():
            res = includes_re.match(lN)
            if res is not None:
                depfn = res.groups()[0]
                if os.path.exists(os.path.join(self.buildpath, depfn)) and depfn not in self.deps:
                    self.deps.append(depfn)

    def concat_deps(self, bn):
        # read source
        src = open(os.path.join(self.buildpath, bn), "r", encoding='utf-8', errors='ignore').read()
        # update direct dependencies
        self.append_cfile_deps(src, self.deps)
        # recurse through deps
        # TODO detect cicular deps.
        if bn in self.deps:
            return src
        return reduce(operator.concat, list(map(self.concat_deps, self.deps)), src)

    def check_and_update_hash_and_deps(self, bn):
        # Get latest computed hash and deps
        oldhash, self.deps = self.srcmd5.get(bn, (None, []))
        # read source
        src = open(os.path.join(self.buildpath, bn), encoding='utf-8', errors='ignore').read()
        # compute new hash
        newhash = hashlib.md5(src.encode()).hexdigest()
        # compare
        match = (oldhash == newhash)
        if not match:
            # file have changed
            # update direct dependencies
            self.append_cfile_deps(src, self.deps)
            # store that hashand deps
            self.srcmd5[bn] = (newhash, self.deps)
        # recurse through deps
        # TODO detect cicular deps.
        # return reduce(operator.and_, list(map(self.check_and_update_hash_and_deps, deps)), match)
        return match

    def calc_source_md5(self):
        wholesrcdata = ""
        for _Location, CFilesAndCFLAGS, _DoCalls in self.CTRInstance.LocationCFilesAndCFLAGS:
            # Get CFiles list to give it to makefile
            for CFile, _CFLAGS in CFilesAndCFLAGS:
                CFileName = os.path.basename(CFile)
                wholesrcdata += self.concat_deps(CFileName)
        return hashlib.md5(wholesrcdata.encode()).hexdigest()

    def calc_md5(self):
        return hashlib.md5(self.GetBinaryCode()).hexdigest()

    async def build(self, model=None):
        res = False
        if not git().version():
            self.CTRInstance.logger.warning("Git not found!")
        old_path = os.getcwd()
        os.chdir(self.prjpath)
        try:
            # f = 'src/POUS.c'
            # if exists(f):
            #     os.remove(f)
            md5key = self.calc_source_md5()
            # 生成platformio.ini
            self.getBoard(model)

            appname = self.CTRInstance.GetProject().getcontentHeader()
            appname = lazy_pinyin(appname['projectName'])
            appname = ''.join(appname)
            appname = appname.replace(" ", "")
            build_flags = self.build_flags + self.CTRInstance.LDFLAGS + ['-DSWNAME=\\"%s\\"' % appname,
                                                                         '-DPLC_MD5=%s' % md5key]

            config = self.CTRInstance.GetTarget().getcontent()
            optimization = config.optimization
            libs = self.libs + self.CTRInstance.Libs
            if self.mode == 'APP' and 'modbus' in libs:
                libs.remove('modbus')
            if 'plc_app' in libs:
                libs.remove('plc_app')
            deps = self.deps
            # 检查
            # if 'IP' not in attrib:
            #     raise Exception(_("Please input default ip!"))

            def_dict = {'LOCAL_PACKAGE': True, }
            if hasattr(config, 'ONLINE_DEBUG') and config.ONLINE_DEBUG:
                def_dict.update({'ONLINE_DEBUG': config.ONLINE_DEBUG})
            packages = package_get(self.CTRInstance.logger, def_dict, libs, staticPackage if self.static else [],
                                   self.app_path)
            package_all = package_get(self.CTRInstance.logger, def_dict, libs + self.rte.lib, [], self.app_path)
            if self.rte.name != "不使用固件":
                packages += [os.path.realpath(os.path.join(self.rte.path, 'plc_app.zip')).replace('\\', '/')]
                package_all += [os.path.realpath(os.path.join(self.rte.path, 'plc_app.zip')).replace('\\', '/')]
            platform = self.getPlatform()
            for att in def_dict:
                val = def_dict[att]
                if val:
                    build_flags.append('-D%s' % att)
                    extra = extra_flag.get(att) or []
                    for e in extra:
                        build_flags.append('-D%s' % e)
            build_flags = list(set(build_flags))
            build_flags_all = self.rte.LDFLAGS
            lib_deps_all = self.rte.deps
            self.platformio_ini(platform, self.framework, self.board, build_flags, package_prog=packages, deps=deps,
                                build_flags_all=build_flags_all, lib_deps_all=lib_deps_all,
                                package_test=package_all, upload_protocol=self.upload_protocol, options=self.options)

            # shutil.copy(os.path.join(self.app_path, 'rtconfig.h'), 'src/rtconfig.h')
            # shutil.copy(os.path.join(self.app_path, 'rtconfig_cm3.h'), 'src/rtconfig_cm3.h')
            # shutil.copy(os.path.join(self.app_path, 'rtconfig_stm32.h'), 'src/rtconfig_stm32.h')
            # if not exists('src/spiffs_config.h'):
            #     shutil.copy(os.path.join(self.app_path, 'spiffs_config.h'), 'src/spiffs_config.h')
            shutil.copy(os.path.join(os.path.dirname(__file__), 'extra_script.py'), 'extra_script.py')
            shutil.copy(os.path.join(os.path.dirname(__file__), 'post_script.py'), 'post_script.py')
            # shutil.copy(os.path.join(self.app_path, 'tool', 'makefsdata.exe'), 'makefsdata.exe')
            # shutil.copy(os.path.join(self.app_path, 'tool', 'msvcr100d.dll'), 'msvcr100d.dll')

            # 输出buildnumber

            buildnumber = self.build_json()
            build_number(buildnumber)
            self.CTRInstance.logger.info(_("Compiling ... \n"))
            if optimization:
                with futures.ThreadPoolExecutor() as executor:
                    ctx = Context(Command('run'))
                    future = executor.submit(run, ctx, environment=[], target=[], upload_port=None,
                                             project_dir=self.prjpath, project_conf=None, jobs=16, silent=False,
                                             verbose=False, disable_auto_clean=True, list_targets=False,
                                             monitor_port=None, program_args=None,
                                             logger=self.CTRInstance.logger)
                    status = await sync_to_async(future.result)()
            else:
                with futures.ThreadPoolExecutor() as executor:
                    ctx = Context(Command('debug'))
                    future = executor.submit(debug, ctx, environment=[], project_dir=self.prjpath, project_conf=None,
                                             load_mode=None, verbose=False, interface=None,
                                             logger=self.CTRInstance.logger)
                    status = await sync_to_async(future.result)()

            if not status:
                self.CTRInstance.logger.error(_("C compilation  failed.\n"))
                os.chdir(old_path)
            else:
                path = self.make_out_dir()
                if path:
                    self.saveInfo(path)
            f = open(self._GetMD5FileName(), "w", encoding='utf-8')
            f.write(md5key)
            f.close()
            self.OutoutIECVariables()
            res = True
        except Exception as e:
            logger.error(traceback.format_exc())
            self.CTRInstance.logger.error(str(e))
        os.chdir(old_path)
        return res

    def platformio_ini(self, platform, framework, board, build_flags, package_prog, deps, package_test, build_flags_all,
                       lib_deps_all,
                       upload_protocol=None, options=[]):
        env = Environment(loader=FileSystemLoader(searchpath=os.path.dirname(__file__)))
        ps = env.get_template("platformio.ini")
        txt = ps.render(platform=platform, framework=framework, model=board, lib_deps=deps,
                        lib_path=os.path.join(self.app_path, 'lib'),
                        core_dir=os.path.join(self.app_path, '.platformio'),
                        library_dir=os.path.join(self.app_path, 'library'), build_flags=build_flags,
                        build_flags_all=build_flags_all, lib_deps_all=lib_deps_all,
                        package_prog=package_prog, package_test=package_test,
                        upload_protocol=upload_protocol, options=options)
        overwrite = True
        if exists('platformio.ini'):
            with open('platformio.ini', 'r', encoding='utf-8') as output:
                t = output.read()
                if t == txt:
                    overwrite = False
        if overwrite:
            with open('platformio.ini', 'w', encoding='utf-8') as output:
                output.write(txt)
            self.CTRInstance.logger.info(_("reBuild config File.\n"))
        else:
            self.CTRInstance.logger.info(_("config File no change.\n"))

    def getPlatform(self):
        if self.static:
            return self.platform_static
        else:
            return self.platform_dev

    def getBoard(self, model=None):
        if model is None:
            boardNode = self.CTRInstance.GetChildByName('board_0')
            if not boardNode:
                self.CTRInstance.logger.error(_('Can not Found "board_0",Please attach one board.'))
                return False
            board = boardNode.board_class
            self.rte = boardNode.current_rte
            self.app = boardNode.current_plc
        else:
            board = boards[model].board()
            self.rte = board.rte()
            self.app = board.plc()
        self.model = board.model
        self.board = 'board = %s' % board.board
        self.upload_protocol = board.upload_protocol
        self.framework = ""
        if not self.rte or (self.rte.name != "不使用固件" and not self.rte.current_ver):
            raise Exception(_('Please select one Firmware.'))
        self.build_flags += board.LDFLAGS + self.app.LDFLAGS or []
        self.libs = self.app.lib or []
        self.deps = self.app.deps or []
        self.lib_other = board.lib_other or []
        self.options = self.app.options or []
        if self.rte.bin:
            self.mode = 'APP'
        else:
            self.mode = 'PLC'
        if self.app.framework:
            self.framework = 'framework = %s' % self.app.framework

    def build_json(self):
        json_build = 'build.json'
        if exists(json_build):
            with open(json_build, 'r', encoding='utf-8') as f:
                build = json.load(f)
                build['build'] += 1
                # if build.get('platform') != platform:
                #     build['platform'] = platform
                #     self.CTRInstance.logger.info(_("Platform Changed . Clean Project ... \n"))
                # os.system("runas /user:Administrator rd /s /q src")
                os.system("rd /s /q .pio")
            with open(json_build, 'w', encoding='utf-8') as f:
                json.dump(build, f)
        else:
            build = {'build': 1}
            with open(json_build, 'w', encoding='utf-8') as f:
                json.dump(build, f)
        return build['build']

    async def debug_start(self):
        pass  # 调试前启动目标相关设置

    def OutoutIECVariables(self):
        """
        Parse CSV-like file  VARIABLES.csv resulting from IEC2C compiler.
        Each section is marked with a line staring with '//'
        list of all variables used in various POUs
        """
        try:
            csvfile = os.path.join(self.CTRInstance._getBuildPath(), "VARIABLES.csv")
            if not exists(csvfile): return
            # describes CSV columns
            ProgramsListAttributeName = ["num", "C_path", "type"]
            VariablesListAttributeName = [
                "num", "vartype", "IEC_path", "C_path", "type", "derived"]
            _ProgramList = []
            _VariablesList = []
            _DbgVariablesList = []
            _IECPathToIdx = {}

            # Separate sections
            ListGroup = []
            for line in open(csvfile, 'r', encoding='utf-8').readlines():
                strippedline = line.strip()
                if strippedline.startswith("//"):
                    # Start new section
                    ListGroup.append([])
                elif len(strippedline) > 0 and len(ListGroup) > 0:
                    # append to this section
                    ListGroup[-1].append(strippedline)

            # first section contains programs
            for line in ListGroup[0]:
                # Split and Maps each field to dictionnary entries
                attrs = dict(
                    list(zip(ProgramsListAttributeName, line.strip().split(';'))))
                # Truncate "C_path" to remove conf an resources names
                attrs["C_path"] = '__'.join(
                    attrs["C_path"].split(".", 2)[1:])
                # Push this dictionnary into result.
                _ProgramList.append(attrs)

            # second section contains all variables
            config_FBs = {}
            Idx = 0
            for line in ListGroup[1]:
                # Split and Maps each field to dictionnary entries
                attrs = dict(
                    list(zip(VariablesListAttributeName, line.strip().split(';'))))
                # Truncate "C_path" to remove conf an resources names
                parts = attrs["C_path"].split(".", 2)
                if len(parts) > 2:
                    config_FB = config_FBs.get(tuple(parts[:2]))
                    if config_FB:
                        parts = [config_FB] + parts[2:]
                        attrs["C_path"] = '.'.join(parts)
                    else:
                        attrs["C_path"] = '__'.join(parts[1:])
                else:
                    attrs["C_path"] = '__'.join(parts)
                    if attrs["vartype"] == "FB":
                        config_FBs[tuple(parts)] = attrs["C_path"]
                if attrs["vartype"] not in ["FB", "EXT", "TEMP"] and attrs["type"] in DebugTypesSize:
                    # todo:获取变量注释
                    # var=self.CTRInstance.Get
                    # Push this dictionnary into result.
                    _DbgVariablesList.append(attrs)
                    # Fill in IEC<->C translation dicts
                    IEC_path = attrs["IEC_path"]
                    _IECPathToIdx[IEC_path] = (Idx, attrs["type"])
                    # Ignores numbers given in CSV file
                    # Idx=int(attrs["num"])
                    # Count variables only, ignore FBs
                    Idx += 1
                _VariablesList.append(attrs)

            # third section contains ticktime
            if len(ListGroup) > 2:
                _Ticktime = int(ListGroup[2][0])
            hfile = os.path.join(self.CTRInstance._getBuildPath(), "VARIABLES.h")
            with open(hfile, "w+", encoding="utf-8") as fp:
                inx = 0
                for line in _DbgVariablesList:
                    ln = "#define %s\t%d\t//%s" % (line["IEC_path"].replace(".", "_"), inx, line["type"])
                    fp.write("%s\n" % ln)
                    inx += 1
        except Exception:
            logger.error(_("Cannot open/parse VARIABLES.csv!\n"))
            logger.error(traceback.format_exc())
            return False

    def GetBuildNumber(self):
        json_build = os.path.join(self.prjpath, 'build.json')
        if exists(json_build):
            with open(json_build, 'r', encoding='utf-8') as f:
                build = json.load(f)
                return build['build']

    def make_out_dir(self):
        try:
            build = self.GetBuildNumber()
            path = os.path.realpath(os.path.join(self.CTRInstance._getBuildPath(), '..', f"build_{build}"))
            os.mkdir(path)
            shutil.copy(self.GetBinaryPath(), path)
            self.CTRInstance.logger.info(f"目标文件已备份至{path}\n")
            return path
        except Exception as ex:
            self.CTRInstance.logger.error(str(ex))
            logger.error(traceback.format_exc())

    def saveInfo(self, path):
        fn = os.path.join(path, 'info.txt')
        with open(fn, 'w+', encoding='utf-8') as fp:
            sw_ver = globalOBJ.appVersion
            fp.write(f"{sw_ver}")
